import { expect } from "chai";
import { FirestoreApi } from "./api";
import { FirebaseError } from "../error";
import * as API from "./api-types";
import { ApiScope, DatabaseEdition, Density } from "./api-types";
import * as Spec from "./api-spec";
import * as sort from "./api-sort";

const idx = new FirestoreApi();

const VALID_SPEC = {
  indexes: [
    {
      collectionGroup: "collection",
      queryScope: "COLLECTION",
      fields: [
        { fieldPath: "foo", order: "ASCENDING" },
        { fieldPath: "bar", order: "DESCENDING" },
        { fieldPath: "baz", arrayConfig: "CONTAINS" },
      ],
    },
  ],
  fieldOverrides: [
    {
      collectionGroup: "collection",
      fieldPath: "foo",
      indexes: [
        { order: "ASCENDING", scope: "COLLECTION" },
        { arrayConfig: "CONTAINS", scope: "COLLECTION" },
      ],
    },
  ],
};

describe("IndexValidation", () => {
  it("should accept a valid v1beta2 index spec", () => {
    idx.validateSpec(VALID_SPEC);
  });

  it("should accept a valid index spec with apiScope, density, and multikey", () => {
    const spec = {
      indexes: [
        {
          collectionGroup: "collection",
          queryScope: "COLLECTION",
          apiScope: "ANY_API",
          density: "DENSE",
          multikey: true,
          fields: [
            { fieldPath: "foo", order: "ASCENDING" },
            { fieldPath: "bar", order: "DESCENDING" },
            { fieldPath: "baz", arrayConfig: "CONTAINS" },
          ],
        },
      ],
    };
    idx.validateSpec(spec);
  });

  it("should reject an index spec with invalid apiScope", () => {
    const spec = {
      indexes: [
        {
          collectionGroup: "collection",
          queryScope: "COLLECTION",
          apiScope: "UNKNOWN",
          fields: [],
        },
      ],
    };
    expect(() => {
      idx.validateSpec(spec);
    }).to.throw(
      FirebaseError,
      /Field "apiScope" must be one of ANY_API, DATASTORE_MODE_API, MONGODB_COMPATIBLE_API/,
    );
  });

  it("should reject an index spec with invalid density", () => {
    const spec = {
      indexes: [
        {
          collectionGroup: "collection",
          queryScope: "COLLECTION",
          apiScope: "ANY_API",
          density: "UNKNOWN",
          fields: [],
        },
      ],
    };
    expect(() => {
      idx.validateSpec(spec);
    }).to.throw(
      FirebaseError,
      /Field "density" must be one of DENSITY_UNSPECIFIED, SPARSE_ALL, SPARSE_ANY, DENSE/,
    );
  });

  it("should reject an index spec with invalid multikey", () => {
    const spec = {
      indexes: [
        {
          collectionGroup: "collection",
          queryScope: "COLLECTION",
          apiScope: "ANY_API",
          density: "DENSE",
          multikey: "multikey",
          fields: [],
        },
      ],
    };
    expect(() => {
      idx.validateSpec(spec);
    }).to.throw(FirebaseError, /Property "multikey" must be of type boolean/);
  });

  it("should reject an index spec with invalid unique", () => {
    const spec = {
      indexes: [
        {
          collectionGroup: "collection",
          queryScope: "COLLECTION",
          apiScope: "ANY_API",
          density: "DENSE",
          multikey: true,
          unique: "true",
          fields: [],
        },
      ],
    };
    expect(() => {
      idx.validateSpec(spec);
    }).to.throw(FirebaseError, /Property "unique" must be of type boolean/);
  });

  it("should not change a valid v1beta2 index spec after upgrade", () => {
    const upgraded = idx.upgradeOldSpec(VALID_SPEC);
    expect(upgraded).to.eql(VALID_SPEC);
  });

  it("should accept an empty spec", () => {
    const empty = {
      indexes: [],
    };

    idx.validateSpec(idx.upgradeOldSpec(empty));
  });

  it("should accept a valid v1beta1 index spec after upgrade", () => {
    idx.validateSpec(
      idx.upgradeOldSpec({
        indexes: [
          {
            collectionId: "collection",
            fields: [
              { fieldPath: "foo", mode: "ASCENDING" },
              { fieldPath: "bar", mode: "DESCENDING" },
              { fieldPath: "baz", mode: "ARRAY_CONTAINS" },
            ],
          },
        ],
      }),
    );
  });

  it("should accept a valid vectorConfig index", () => {
    idx.validateSpec(
      idx.upgradeOldSpec({
        indexes: [
          {
            collectionGroup: "collection",
            queryScope: "COLLECTION",
            fields: [
              {
                fieldPath: "embedding",
                vectorConfig: {
                  dimension: 100,
                  flat: {},
                },
              },
            ],
          },
        ],
      }),
    );
  });

  it("should accept a valid vectorConfig index after upgrade", () => {
    idx.validateSpec({
      indexes: [
        {
          collectionGroup: "collection",
          queryScope: "COLLECTION",
          fields: [
            {
              fieldPath: "embedding",
              vectorConfig: {
                dimension: 100,
                flat: {},
              },
            },
          ],
        },
      ],
    });
  });

  it("should accept a valid vectorConfig index with another field", () => {
    idx.validateSpec({
      indexes: [
        {
          collectionGroup: "collection",
          queryScope: "COLLECTION",
          fields: [
            { fieldPath: "foo", order: "ASCENDING" },
            {
              fieldPath: "embedding",
              vectorConfig: {
                dimension: 100,
                flat: {},
              },
            },
          ],
        },
      ],
    });
  });

  it("should reject invalid vectorConfig dimension", () => {
    expect(() => {
      idx.validateSpec({
        indexes: [
          {
            collectionGroup: "collection",
            queryScope: "COLLECTION",
            fields: [
              {
                fieldPath: "embedding",
                vectorConfig: {
                  dimension: "wrongType",
                  flat: {},
                },
              },
            ],
          },
        ],
      });
    }).to.throw(FirebaseError, /Property "vectorConfig.dimension" must be of type number/);
  });

  it("should reject invalid vectorConfig missing flat type", () => {
    expect(() => {
      idx.validateSpec({
        indexes: [
          {
            collectionGroup: "collection",
            queryScope: "COLLECTION",
            fields: [
              {
                fieldPath: "embedding",
                vectorConfig: {
                  dimension: 100,
                },
              },
            ],
          },
        ],
      });
    }).to.throw(FirebaseError, /Must contain "flat"/);
  });

  it("should reject an incomplete index spec", () => {
    expect(() => {
      idx.validateSpec({
        indexes: [
          {
            collectionGroup: "collection",
            fields: [
              { fieldPath: "foo", order: "ASCENDING" },
              { fieldPath: "bar", order: "DESCENDING" },
            ],
          },
        ],
      });
    }).to.throw(FirebaseError, /Must contain "queryScope"/);
  });

  it("should reject an overspecified index spec", () => {
    expect(() => {
      idx.validateSpec({
        indexes: [
          {
            collectionGroup: "collection",
            queryScope: "COLLECTION",
            fields: [
              { fieldPath: "foo", order: "ASCENDING", arrayConfig: "CONTAINES" },
              { fieldPath: "bar", order: "DESCENDING" },
            ],
          },
        ],
      });
    }).to.throw(FirebaseError, /Must contain exactly one of "order,arrayConfig,vectorConfig"/);
  });
});
describe("IndexSpecMatching", () => {
  const baseApiIndex: API.Index = {
    name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123",
    queryScope: API.QueryScope.COLLECTION,
    fields: [{ fieldPath: "__name__", order: API.Order.ASCENDING }],
  };

  const baseSpecIndex: Spec.Index = {
    collectionGroup: "collection",
    queryScope: "COLLECTION",
    fields: [{ fieldPath: "__name__", order: API.Order.ASCENDING }],
  } as Spec.Index;

  it("should identify a positive index spec match", () => {
    const apiIndex: API.Index = {
      name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123",
      queryScope: API.QueryScope.COLLECTION,
      fields: [
        { fieldPath: "foo", order: API.Order.ASCENDING },
        { fieldPath: "bar", arrayConfig: API.ArrayConfig.CONTAINS },
        { fieldPath: "baz", vectorConfig: { dimension: 384, flat: {} } },
        { fieldPath: "__name__", order: API.Order.ASCENDING },
      ],
      state: API.State.READY,
    };

    const specIndex = {
      collectionGroup: "collection",
      queryScope: "COLLECTION",
      fields: [
        { fieldPath: "foo", order: "ASCENDING" },
        { fieldPath: "bar", arrayConfig: "CONTAINS" },
        { fieldPath: "baz", vectorConfig: { dimension: 384, flat: {} } },
        { fieldPath: "__name__", order: API.Order.ASCENDING },
      ],
    } as Spec.Index;

    expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(true);
    expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(true);
  });

  it("should identify a negative index spec match with different vector config dimension", () => {
    const apiIndex = {
      ...baseApiIndex,
      fields: [{ fieldPath: "baz", vectorConfig: { dimension: 384, flat: {} } }],
    };

    const specIndex = {
      ...baseSpecIndex,
      fields: [{ fieldPath: "baz", vectorConfig: { dimension: 382, flat: {} } }],
    } as Spec.Index;

    expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(false);
    expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(false);
  });

  it("should identify a positive index spec match with apiScope, density, multikey, and unique", () => {
    const apiIndex: API.Index = {
      name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123",
      queryScope: API.QueryScope.COLLECTION,
      apiScope: API.ApiScope.ANY_API,
      density: API.Density.DENSE,
      multikey: true,
      unique: true,
      fields: [
        { fieldPath: "foo", order: API.Order.ASCENDING },
        { fieldPath: "bar", arrayConfig: API.ArrayConfig.CONTAINS },
      ],
      state: API.State.READY,
    };

    const specIndex = {
      collectionGroup: "collection",
      queryScope: "COLLECTION",
      apiScope: "ANY_API",
      density: "DENSE",
      multikey: true,
      unique: true,
      fields: [
        { fieldPath: "foo", order: "ASCENDING" },
        { fieldPath: "bar", arrayConfig: "CONTAINS" },
      ],
    } as Spec.Index;

    expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(true);
  });

  it("should identify a positive index spec match when missing apiScope and density and multikey in both", () => {
    expect(idx.indexMatchesSpec(baseApiIndex, baseSpecIndex, DatabaseEdition.STANDARD)).to.eql(
      true,
    );
    expect(idx.indexMatchesSpec(baseApiIndex, baseSpecIndex, DatabaseEdition.ENTERPRISE)).to.eql(
      true,
    );
  });

  describe("ApiScope", () => {
    it("should identify a negative index spec match with different apiScope", () => {
      const apiIndex = { ...baseApiIndex, apiScope: API.ApiScope.ANY_API };
      const specIndex = { ...baseSpecIndex, apiScope: "MONGODB_COMPATIBLE_API" } as Spec.Index;
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(false);
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(false);
    });

    it("should identify a positive index spec match with same apiScope ANY_API", () => {
      const apiIndex = { ...baseApiIndex, apiScope: API.ApiScope.ANY_API };
      const specIndex = { ...baseSpecIndex, apiScope: "ANY_API" } as Spec.Index;
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(true);
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(true);
    });

    it("should identify a positive index spec match with same apiScope MONGODB_COMPATIBLE_API", () => {
      const apiIndex = { ...baseApiIndex, apiScope: API.ApiScope.MONGODB_COMPATIBLE_API };
      const specIndex = { ...baseSpecIndex, apiScope: "MONGODB_COMPATIBLE_API" } as Spec.Index;
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(true);
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(true);
    });

    it("should identify a positive match, apiScope missing in index, default in spec", () => {
      const apiIndex = baseApiIndex;
      const specIndex = { ...baseSpecIndex, apiScope: "ANY_API" } as Spec.Index;
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(true);
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(true);
    });

    it("should identify a positive match, apiScope missing in spec, default in index", () => {
      const apiIndex = { ...baseApiIndex, apiScope: ApiScope.ANY_API };
      const specIndex = baseSpecIndex;
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(true);
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(true);
    });

    it("should identify a negative match, apiScope missing in index, non-default in spec", () => {
      const apiIndex = baseApiIndex;
      const specIndex = { ...baseSpecIndex, apiScope: "MONGODB_COMPATIBLE_API" } as Spec.Index;
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(false);
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(false);
    });

    it("should identify a negative match, apiScope missing in spec, non-default in index", () => {
      const apiIndex = { ...baseApiIndex, apiScope: ApiScope.MONGODB_COMPATIBLE_API };
      const specIndex = baseSpecIndex;
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(false);
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(false);
    });
  });

  describe("Density", () => {
    it("should identify a negative index spec match with different density", () => {
      const apiIndex = { ...baseApiIndex, density: API.Density.DENSE };
      const specIndex = { ...baseSpecIndex, density: "SPARSE_ALL" } as Spec.Index;
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(false);
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(false);
    });

    it("should identify a positive index spec match with same density DENSE", () => {
      const apiIndex = { ...baseApiIndex, density: API.Density.DENSE };
      const specIndex = { ...baseSpecIndex, density: "DENSE" } as Spec.Index;
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(true);
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(true);
    });

    it("should identify a positive index spec match with same density SPARSE_ALL", () => {
      const apiIndex = { ...baseApiIndex, density: API.Density.SPARSE_ALL };
      const specIndex = { ...baseSpecIndex, density: "SPARSE_ALL" } as Spec.Index;
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(true);
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(true);
    });

    it("should identify a positive index spec match with same density SPARSE_ANY", () => {
      const apiIndex = { ...baseApiIndex, density: API.Density.SPARSE_ANY };
      const specIndex = { ...baseSpecIndex, density: "SPARSE_ANY" } as Spec.Index;
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(true);
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(true);
    });

    it("should identify a positive match, density missing in index, default in spec", () => {
      // The default value for Enterprise is DENSE
      const apiIndex1 = baseApiIndex;
      const specIndex1 = { ...baseSpecIndex, density: "DENSE" } as Spec.Index;
      expect(idx.indexMatchesSpec(apiIndex1, specIndex1, DatabaseEdition.STANDARD)).to.eql(false);
      expect(idx.indexMatchesSpec(apiIndex1, specIndex1, DatabaseEdition.ENTERPRISE)).to.eql(true);

      // The default value for Standard is SPARSE_ALL
      const apiIndex = baseApiIndex;
      const specIndex = { ...baseSpecIndex, density: "SPARSE_ALL" } as Spec.Index;
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(true);
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(false);
    });

    it("should identify a positive match, density missing in spec, default in index", () => {
      // The default value for Enterprise is DENSE
      const apiIndex1 = { ...baseApiIndex, density: Density.DENSE };
      const specIndex1 = baseSpecIndex;
      expect(idx.indexMatchesSpec(apiIndex1, specIndex1, DatabaseEdition.STANDARD)).to.eql(false);
      expect(idx.indexMatchesSpec(apiIndex1, specIndex1, DatabaseEdition.ENTERPRISE)).to.eql(true);

      // The default value for Standard is SPARSE_ALL
      const apiIndex2 = { ...baseApiIndex, density: Density.SPARSE_ALL };
      const specIndex2 = baseSpecIndex;
      expect(idx.indexMatchesSpec(apiIndex2, specIndex2, DatabaseEdition.STANDARD)).to.eql(true);
      expect(idx.indexMatchesSpec(apiIndex2, specIndex2, DatabaseEdition.ENTERPRISE)).to.eql(false);
    });

    it("should identify a negative match, density missing in index, non-default in spec", () => {
      // The default value for Enterprise is DENSE, and the default value for Standard is SPARSE_ALL
      // so using SPARSE_ANY should fail both.
      const apiIndex = baseApiIndex;
      const specIndex = { ...baseSpecIndex, density: "SPARSE_ANY" } as Spec.Index;
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(false);
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(false);
    });

    it("should identify a negative match, density missing in spec, non-default in index", () => {
      // The default value for Enterprise is DENSE, and the default value for Standard is SPARSE_ALL
      // so using SPARSE_ANY should fail both.
      const apiIndex1 = { ...baseApiIndex, density: Density.SPARSE_ANY };
      const specIndex1 = baseSpecIndex;
      expect(idx.indexMatchesSpec(apiIndex1, specIndex1, DatabaseEdition.STANDARD)).to.eql(false);
      expect(idx.indexMatchesSpec(apiIndex1, specIndex1, DatabaseEdition.ENTERPRISE)).to.eql(false);
    });
  });

  describe("Multikey", () => {
    it("should identify a negative index spec match with different multikey", () => {
      const apiIndex = { ...baseApiIndex, multikey: true };
      const specIndex = { ...baseSpecIndex, multikey: false } as Spec.Index;
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(false);
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(false);
    });

    it("should identify a positive index spec match with same multikey true", () => {
      const apiIndex = { ...baseApiIndex, multikey: true };
      const specIndex = { ...baseSpecIndex, multikey: true } as Spec.Index;
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(true);
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(true);
    });

    it("should identify a positive index spec match with same multikey false", () => {
      const apiIndex = { ...baseApiIndex, multikey: false };
      const specIndex = { ...baseSpecIndex, multikey: false } as Spec.Index;
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(true);
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(true);
    });

    it("should identify a positive match, multikey missing in index, default in spec", () => {
      const apiIndex = baseApiIndex;
      const specIndex = { ...baseSpecIndex, multikey: false } as Spec.Index;
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(true);
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(true);
    });

    it("should identify a positive match, multikey missing in spec, default in index", () => {
      const apiIndex = { ...baseApiIndex, multikey: false };
      const specIndex = baseSpecIndex;
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(true);
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(true);
    });

    it("should identify a negative match, multikey missing in index, non-default in spec", () => {
      const apiIndex = baseApiIndex;
      const specIndex = { ...baseSpecIndex, multikey: true } as Spec.Index;
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(false);
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(false);
    });

    it("should identify a negative match, multikey missing in spec, non-default in index", () => {
      const apiIndex = { ...baseApiIndex, multikey: true };
      const specIndex = baseSpecIndex;
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(false);
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(false);
    });
  });

  describe("With __name__ field in index", () => {
    it("__name__ field with default sort order, identified as matching", () => {
      const apiIndex = {
        name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123",
        queryScope: "COLLECTION",
        fields: [
          { fieldPath: "foo", order: "ASCENDING" },
          { fieldPath: "__name__", order: "ASCENDING" },
        ],
        state: API.State.READY,
      } as API.Index;

      const specIndex = {
        collectionGroup: "collection",
        queryScope: "COLLECTION",
        fields: [
          { fieldPath: "foo", order: "ASCENDING" },
          { fieldPath: "__name__", order: "ASCENDING" },
        ],
      } as Spec.Index;

      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(true);
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(true);
    });

    it("__name__ field with default sort order stripped off, identified as matching if STANDARD", () => {
      const apiIndex = {
        name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123",
        queryScope: "COLLECTION",
        fields: [
          { fieldPath: "foo", order: "ASCENDING" },
          { fieldPath: "__name__", order: "ASCENDING" },
        ],
        state: API.State.READY,
      } as API.Index;

      const specIndex = {
        collectionGroup: "collection",
        queryScope: "COLLECTION",
        fields: [{ fieldPath: "foo", order: "ASCENDING" }],
      } as Spec.Index;

      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(false);
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(true);
    });

    it("__name__ field with non-default sort order, identified as matching", () => {
      const apiIndex = {
        name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123",
        queryScope: "COLLECTION",
        fields: [
          { fieldPath: "foo", order: "ASCENDING" },
          { fieldPath: "__name__", order: "DESCENDING" },
        ],
        state: API.State.READY,
      } as API.Index;

      const specIndex = {
        collectionGroup: "collection",
        queryScope: "COLLECTION",
        fields: [
          { fieldPath: "foo", order: "ASCENDING" },
          { fieldPath: "__name__", order: "DESCENDING" },
        ],
      } as Spec.Index;

      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(true);
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(true);
    });

    it("__name__ field sort order mismatch, identified as not matching", () => {
      const apiIndex = {
        name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123",
        queryScope: "COLLECTION",
        fields: [
          { fieldPath: "foo", order: "ASCENDING" },
          { fieldPath: "__name__", order: "ASCENDING" },
        ],
        state: API.State.READY,
      } as API.Index;

      const specIndex = {
        collectionGroup: "collection",
        queryScope: "COLLECTION",
        fields: [
          { fieldPath: "foo", order: "ASCENDING" },
          { fieldPath: "__name__", order: "DESCENDING" },
        ],
      } as Spec.Index;

      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(false);
      expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(false);
    });
  });

  it("should identify a negative index spec match", () => {
    const apiIndex = {
      name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123",
      queryScope: "COLLECTION",
      fields: [
        { fieldPath: "foo", order: "DESCENDING" },
        { fieldPath: "bar", arrayConfig: "CONTAINS" },
      ],
      state: API.State.READY,
    } as API.Index;

    const specIndex = {
      collectionGroup: "collection",
      queryScope: "COLLECTION",
      fields: [
        { fieldPath: "foo", order: "ASCENDING" },
        { fieldPath: "bar", arrayConfig: "CONTAINS" },
      ],
    } as Spec.Index;

    // The second spec contains ASCENDING where the former contains DESCENDING
    expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(false);
    expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(false);
  });

  it("should identify a positive field spec match", () => {
    const apiField = {
      name: "/projects/myproject/databases/(default)/collectionGroups/collection/fields/abc123",
      indexConfig: {
        indexes: [
          {
            queryScope: "COLLECTION",
            fields: [{ fieldPath: "abc123", order: "ASCENDING" }],
          },
          {
            queryScope: "COLLECTION",
            fields: [{ fieldPath: "abc123", arrayConfig: "CONTAINS" }],
          },
        ],
      },
    } as API.Field;

    const specField = {
      collectionGroup: "collection",
      fieldPath: "abc123",
      indexes: [
        { order: "ASCENDING", queryScope: "COLLECTION" },
        { arrayConfig: "CONTAINS", queryScope: "COLLECTION" },
      ],
    } as Spec.FieldOverride;

    expect(idx.fieldMatchesSpec(apiField, specField)).to.eql(true);
  });

  it("should identify a positive field spec match with ttl specified as false", () => {
    const apiField = {
      name: "/projects/myproject/databases/(default)/collectionGroups/collection/fields/abc123",
      indexConfig: {
        indexes: [
          {
            queryScope: "COLLECTION",
            fields: [{ fieldPath: "abc123", order: "ASCENDING" }],
          },
          {
            queryScope: "COLLECTION",
            fields: [{ fieldPath: "abc123", arrayConfig: "CONTAINS" }],
          },
        ],
      },
    } as API.Field;

    const specField = {
      collectionGroup: "collection",
      fieldPath: "abc123",
      ttl: false,
      indexes: [
        { order: "ASCENDING", queryScope: "COLLECTION" },
        { arrayConfig: "CONTAINS", queryScope: "COLLECTION" },
      ],
    } as Spec.FieldOverride;

    expect(idx.fieldMatchesSpec(apiField, specField)).to.eql(true);
  });

  it("should identify a positive ttl field spec match", () => {
    const apiField = {
      name: "/projects/myproject/databases/(default)/collectionGroups/collection/fields/fieldTtl",
      indexConfig: {
        indexes: [
          {
            queryScope: "COLLECTION",
            fields: [{ fieldPath: "fieldTtl", order: "ASCENDING" }],
          },
        ],
      },
      ttlConfig: {
        state: "ACTIVE",
      },
    } as API.Field;

    const specField = {
      collectionGroup: "collection",
      fieldPath: "fieldTtl",
      ttl: true,
      indexes: [{ order: "ASCENDING", queryScope: "COLLECTION" }],
    } as Spec.FieldOverride;

    expect(idx.fieldMatchesSpec(apiField, specField)).to.eql(true);
  });

  it("should identify a negative ttl field spec match", () => {
    const apiField = {
      name: "/projects/myproject/databases/(default)/collectionGroups/collection/fields/fieldTtl",
      indexConfig: {
        indexes: [
          {
            queryScope: "COLLECTION",
            fields: [{ fieldPath: "fieldTtl", order: "ASCENDING" }],
          },
        ],
      },
    } as API.Field;

    const specField = {
      collectionGroup: "collection",
      fieldPath: "fieldTtl",
      ttl: true,
      indexes: [{ order: "ASCENDING", queryScope: "COLLECTION" }],
    } as Spec.FieldOverride;

    expect(idx.fieldMatchesSpec(apiField, specField)).to.eql(false);
  });

  it("should match a field spec with all indexes excluded", () => {
    const apiField = {
      name: "/projects/myproject/databases/(default)/collectionGroups/collection/fields/abc123",
      indexConfig: {},
    } as API.Field;

    const specField = {
      collectionGroup: "collection",
      fieldPath: "abc123",
      indexes: [],
    } as Spec.FieldOverride;

    expect(idx.fieldMatchesSpec(apiField, specField)).to.eql(true);
  });

  it("should match a field spec with only ttl", () => {
    const apiField = {
      name: "/projects/myproject/databases/(default)/collectionGroups/collection/fields/ttlField",
      ttlConfig: {
        state: "ACTIVE",
      },
      indexConfig: {},
    } as API.Field;

    const specField = {
      collectionGroup: "collection",
      fieldPath: "ttlField",
      ttl: true,
      indexes: [],
    } as Spec.FieldOverride;

    expect(idx.fieldMatchesSpec(apiField, specField)).to.eql(true);
  });

  it("should identify a negative field spec match", () => {
    const apiField = {
      name: "/projects/myproject/databases/(default)/collectionGroups/collection/fields/abc123",
      indexConfig: {
        indexes: [
          {
            queryScope: "COLLECTION",
            fields: [{ fieldPath: "abc123", order: "ASCENDING" }],
          },
          {
            queryScope: "COLLECTION",
            fields: [{ fieldPath: "abc123", arrayConfig: "CONTAINS" }],
          },
        ],
      },
    } as API.Field;

    const specField = {
      collectionGroup: "collection",
      fieldPath: "abc123",
      indexes: [
        { order: "DESCENDING", queryScope: "COLLECTION" },
        { arrayConfig: "CONTAINS", queryScope: "COLLECTION" },
      ],
    } as Spec.FieldOverride;

    // The second spec contains "DESCENDING" where the first contains "ASCENDING"
    expect(idx.fieldMatchesSpec(apiField, specField)).to.eql(false);
  });

  it("should identify a negative field spec match with ttl as false", () => {
    const apiField = {
      name: "/projects/myproject/databases/(default)/collectionGroups/collection/fields/fieldTtl",
      ttlConfig: {
        state: "ACTIVE",
      },
      indexConfig: {},
    } as API.Field;

    const specField = {
      collectionGroup: "collection",
      fieldPath: "fieldTtl",
      ttl: false,
      indexes: [],
    } as Spec.FieldOverride;

    // The second spec contains "false" for ttl  where the first contains "true"
    // for ttl
    expect(idx.fieldMatchesSpec(apiField, specField)).to.eql(false);
  });
});

describe("Normalize __name__ field for database indexes", () => {
  it("No-op if exists __name__ field as the last field with default sort order", () => {
    const mockSpecIndex = {
      collectionGroup: "collection",
      queryScope: "COLLECTION",
      fields: [
        { fieldPath: "foo", order: API.Order.ASCENDING },
        { fieldPath: "__name__", order: API.Order.ASCENDING },
      ],
    } as Spec.Index;

    const result = FirestoreApi.processIndex(mockSpecIndex);

    expect(result.fields).to.have.length(2);
    expect(result.fields[0].fieldPath).to.equal("foo");
    expect(result.fields[1].fieldPath).to.equal("__name__");
    expect(result.fields[1].order).to.equal(API.Order.ASCENDING);
  });

  it("No-op if exists __name__ field as the last field with default sort order, with array contains", () => {
    const mockSpecIndex = {
      collectionGroup: "collection",
      queryScope: "COLLECTION",
      fields: [
        { fieldPath: "arr", arrayConfig: API.ArrayConfig.CONTAINS },
        { fieldPath: "__name__", order: API.Order.ASCENDING },
      ],
    } as Spec.Index;

    const result = FirestoreApi.processIndex(mockSpecIndex);

    expect(result.fields).to.have.length(2);
    expect(result.fields[0].fieldPath).to.equal("arr");
    expect(result.fields[1].fieldPath).to.equal("__name__");
    expect(result.fields[1].order).to.equal(API.Order.ASCENDING);
  });

  it("No-op if exists __name__ field as the last field with default sort order, with vector field", () => {
    const mockSpecIndex = {
      collectionGroup: "collection",
      queryScope: "COLLECTION",
      fields: [
        { fieldPath: "foo", order: API.Order.ASCENDING },
        { fieldPath: "__name__", order: API.Order.ASCENDING },
        {
          fieldPath: "vector",
          vectorConfig: {
            dimension: 100,
            flat: {},
          },
        },
      ],
    } as Spec.Index;

    const result = FirestoreApi.processIndex(mockSpecIndex);

    expect(result.fields).to.have.length(3);
    expect(result.fields[0].fieldPath).to.equal("foo");
    expect(result.fields[1].fieldPath).to.equal("__name__");
    expect(result.fields[2].fieldPath).to.equal("vector");
    expect(result.fields[1].order).to.equal(API.Order.ASCENDING);
  });

  it("No-op if exists __name__ field as the last field with default sort order, with array contains and vector field", () => {
    const mockSpecIndex = {
      collectionGroup: "collection",
      queryScope: "COLLECTION",
      fields: [
        { fieldPath: "foo", order: API.Order.ASCENDING },
        { fieldPath: "arr", arrayConfig: API.ArrayConfig.CONTAINS },
        { fieldPath: "__name__", order: API.Order.ASCENDING },
        {
          fieldPath: "vector",
          vectorConfig: {
            dimension: 100,
            flat: {},
          },
        },
      ],
    } as Spec.Index;

    const result = FirestoreApi.processIndex(mockSpecIndex);

    expect(result.fields).to.have.length(4);
    expect(result.fields[0].fieldPath).to.equal("foo");
    expect(result.fields[1].fieldPath).to.equal("arr");
    expect(result.fields[2].fieldPath).to.equal("__name__");
    expect(result.fields[3].fieldPath).to.equal("vector");
    expect(result.fields[2].order).to.equal(API.Order.ASCENDING);
  });

  it("No-op if exists __name__ field as the last field with non-default sort order", () => {
    const mockSpecIndex = {
      collectionGroup: "collection",
      queryScope: "COLLECTION",
      fields: [
        { fieldPath: "foo", order: API.Order.ASCENDING },
        { fieldPath: "__name__", order: API.Order.DESCENDING },
      ],
    } as Spec.Index;

    const result = FirestoreApi.processIndex(mockSpecIndex);

    expect(result.fields).to.have.length(2);
    expect(result.fields[0].fieldPath).to.equal("foo");
    expect(result.fields[1].fieldPath).to.equal("__name__");
    expect(result.fields[1].order).to.equal(API.Order.DESCENDING);
  });

  it("No-op if exists __name__ field as the last field with default sort order, with array contains", () => {
    const mockSpecIndex = {
      collectionGroup: "collection",
      queryScope: "COLLECTION",
      fields: [
        { fieldPath: "arr", arrayConfig: API.ArrayConfig.CONTAINS },
        { fieldPath: "__name__", order: API.Order.ASCENDING },
      ],
    } as Spec.Index;

    const result = FirestoreApi.processIndex(mockSpecIndex);

    expect(result.fields).to.have.length(2);
    expect(result.fields[0].fieldPath).to.equal("arr");
    expect(result.fields[1].fieldPath).to.equal("__name__");
    expect(result.fields[1].order).to.equal(API.Order.ASCENDING);
  });

  it("No-op if exists __name__ field as the last field with default sort order, with vector field", () => {
    const mockSpecIndex = {
      collectionGroup: "collection",
      queryScope: "COLLECTION",
      fields: [
        { fieldPath: "foo", order: API.Order.ASCENDING },
        { fieldPath: "__name__", order: API.Order.DESCENDING },
        {
          fieldPath: "vector",
          vectorConfig: {
            dimension: 100,
            flat: {},
          },
        },
      ],
    } as Spec.Index;

    const result = FirestoreApi.processIndex(mockSpecIndex);

    expect(result.fields).to.have.length(3);
    expect(result.fields[0].fieldPath).to.equal("foo");
    expect(result.fields[1].fieldPath).to.equal("__name__");
    expect(result.fields[2].fieldPath).to.equal("vector");
    expect(result.fields[1].order).to.equal(API.Order.DESCENDING);
  });

  it("No-op if exists __name__ field as the last field with default sort order, with array contains and vector field", () => {
    const mockSpecIndex = {
      collectionGroup: "collection",
      queryScope: "COLLECTION",
      fields: [
        { fieldPath: "foo", order: API.Order.ASCENDING },
        { fieldPath: "arr", arrayConfig: API.ArrayConfig.CONTAINS },
        { fieldPath: "__name__", order: API.Order.DESCENDING },
        {
          fieldPath: "vector",
          vectorConfig: {
            dimension: 100,
            flat: {},
          },
        },
      ],
    } as Spec.Index;

    const result = FirestoreApi.processIndex(mockSpecIndex);

    expect(result.fields).to.have.length(4);
    expect(result.fields[0].fieldPath).to.equal("foo");
    expect(result.fields[1].fieldPath).to.equal("arr");
    expect(result.fields[2].fieldPath).to.equal("__name__");
    expect(result.fields[3].fieldPath).to.equal("vector");
    expect(result.fields[2].order).to.equal(API.Order.DESCENDING);
  });

  it("should attach __name__ suffix with the default order if not exists, ascending", () => {
    const mockSpecIndex = {
      collectionGroup: "collection",
      queryScope: "COLLECTION",
      fields: [{ fieldPath: "foo", order: API.Order.ASCENDING }],
    } as Spec.Index;

    const result = FirestoreApi.processIndex(mockSpecIndex);

    expect(result.fields).to.have.length(2);
    expect(result.fields[0].fieldPath).to.equal("foo");
    expect(result.fields[1].fieldPath).to.equal("__name__");
    expect(result.fields[1].order).to.equal(API.Order.ASCENDING);
  });

  it("should attach __name__ suffix with the default order if not exists, ascending, with array contains", () => {
    const mockSpecIndex = {
      collectionGroup: "collection",
      queryScope: "COLLECTION",
      fields: [
        { fieldPath: "foo", order: API.Order.ASCENDING },
        { fieldPath: "arr", arrayConfig: API.ArrayConfig.CONTAINS },
      ],
    } as Spec.Index;

    const result = FirestoreApi.processIndex(mockSpecIndex);

    expect(result.fields).to.have.length(3);
    expect(result.fields[0].fieldPath).to.equal("foo");
    expect(result.fields[1].fieldPath).to.equal("arr");
    expect(result.fields[2].fieldPath).to.equal("__name__");
    expect(result.fields[2].order).to.equal(API.Order.ASCENDING);
  });

  it("should attach __name__ suffix with the default order if not exists, ascending, with vector field", () => {
    const mockSpecIndex = {
      collectionGroup: "collection",
      queryScope: "COLLECTION",
      fields: [
        { fieldPath: "foo", order: API.Order.ASCENDING },
        {
          fieldPath: "vector",
          vectorConfig: {
            dimension: 100,
            flat: {},
          },
        },
      ],
    } as Spec.Index;

    const result = FirestoreApi.processIndex(mockSpecIndex);

    expect(result.fields).to.have.length(3);
    expect(result.fields[0].fieldPath).to.equal("foo");
    expect(result.fields[1].fieldPath).to.equal("__name__");
    expect(result.fields[2].fieldPath).to.equal("vector");
    expect(result.fields[1].order).to.equal(API.Order.ASCENDING);
  });

  it("should attach __name__ suffix with the default order if not exists, with array contains and vector field, default to ascending", () => {
    const mockSpecIndex = {
      collectionGroup: "collection",
      queryScope: "COLLECTION",
      fields: [
        { fieldPath: "arr", arrayConfig: API.ArrayConfig.CONTAINS },
        {
          fieldPath: "vector",
          vectorConfig: {
            dimension: 100,
            flat: {},
          },
        },
      ],
    } as Spec.Index;

    const result = FirestoreApi.processIndex(mockSpecIndex);

    expect(result.fields).to.have.length(3);
    expect(result.fields[0].fieldPath).to.equal("arr");
    expect(result.fields[1].fieldPath).to.equal("__name__");
    expect(result.fields[2].fieldPath).to.equal("vector");
    expect(result.fields[1].order).to.equal(API.Order.ASCENDING);
  });

  it("should attach __name__ suffix with the default order if not exists, with index field with ascending order, array contains and vector field", () => {
    const mockSpecIndex = {
      collectionGroup: "collection",
      queryScope: "COLLECTION",
      fields: [
        { fieldPath: "foo", order: API.Order.ASCENDING },
        { fieldPath: "arr", arrayConfig: API.ArrayConfig.CONTAINS },
        {
          fieldPath: "vector",
          vectorConfig: {
            dimension: 100,
            flat: {},
          },
        },
      ],
    } as Spec.Index;

    const result = FirestoreApi.processIndex(mockSpecIndex);

    expect(result.fields).to.have.length(4);
    expect(result.fields[0].fieldPath).to.equal("foo");
    expect(result.fields[1].fieldPath).to.equal("arr");
    expect(result.fields[2].fieldPath).to.equal("__name__");
    expect(result.fields[3].fieldPath).to.equal("vector");
    expect(result.fields[2].order).to.equal(API.Order.ASCENDING);
  });

  it("should attach __name__ suffix with the default order if not exists, descending", () => {
    const mockSpecIndex = {
      collectionGroup: "collection",
      queryScope: "COLLECTION",
      fields: [
        { fieldPath: "foo", order: API.Order.ASCENDING },
        { fieldPath: "bar", order: API.Order.DESCENDING },
      ],
    } as Spec.Index;

    const result = FirestoreApi.processIndex(mockSpecIndex);

    expect(result.fields).to.have.length(3);
    expect(result.fields[0].fieldPath).to.equal("foo");
    expect(result.fields[1].fieldPath).to.equal("bar");
    expect(result.fields[2].fieldPath).to.equal("__name__");
    expect(result.fields[2].order).to.equal(API.Order.DESCENDING);
  });

  it("should attach __name__ suffix with the default order if not exists, descending, with array contains", () => {
    const mockSpecIndex = {
      collectionGroup: "collection",
      queryScope: "COLLECTION",
      fields: [
        { fieldPath: "foo", order: API.Order.DESCENDING },
        { fieldPath: "arr", arrayConfig: API.ArrayConfig.CONTAINS },
      ],
    } as Spec.Index;

    const result = FirestoreApi.processIndex(mockSpecIndex);

    expect(result.fields).to.have.length(3);
    expect(result.fields[0].fieldPath).to.equal("foo");
    expect(result.fields[1].fieldPath).to.equal("arr");
    expect(result.fields[2].fieldPath).to.equal("__name__");
    expect(result.fields[2].order).to.equal(API.Order.DESCENDING);
  });

  it("should attach __name__ suffix with the default order if not exists, ascending, with vector field", () => {
    const mockSpecIndex = {
      collectionGroup: "collection",
      queryScope: "COLLECTION",
      fields: [
        { fieldPath: "foo", order: API.Order.DESCENDING },
        {
          fieldPath: "vector",
          vectorConfig: {
            dimension: 100,
            flat: {},
          },
        },
      ],
    } as Spec.Index;

    const result = FirestoreApi.processIndex(mockSpecIndex);

    expect(result.fields).to.have.length(3);
    expect(result.fields[0].fieldPath).to.equal("foo");
    expect(result.fields[1].fieldPath).to.equal("__name__");
    expect(result.fields[2].fieldPath).to.equal("vector");
    expect(result.fields[1].order).to.equal(API.Order.DESCENDING);
  });

  it("should attach __name__ suffix with the default order if not exists, with index field with descending order, array contains and vector field", () => {
    const mockSpecIndex = {
      collectionGroup: "collection",
      queryScope: "COLLECTION",
      fields: [
        { fieldPath: "foo", order: API.Order.DESCENDING },
        { fieldPath: "arr", arrayConfig: API.ArrayConfig.CONTAINS },
        {
          fieldPath: "vector",
          vectorConfig: {
            dimension: 100,
            flat: {},
          },
        },
      ],
    } as Spec.Index;

    const result = FirestoreApi.processIndex(mockSpecIndex);

    expect(result.fields).to.have.length(4);
    expect(result.fields[0].fieldPath).to.equal("foo");
    expect(result.fields[1].fieldPath).to.equal("arr");
    expect(result.fields[2].fieldPath).to.equal("__name__");
    expect(result.fields[3].fieldPath).to.equal("vector");
    expect(result.fields[2].order).to.equal(API.Order.DESCENDING);
  });
});

describe("IndexSorting", () => {
  it("should be able to handle empty arrays", () => {
    expect(([] as Spec.Index[]).sort(sort.compareSpecIndex)).to.eql([]);
    expect(([] as Spec.FieldOverride[]).sort(sort.compareFieldOverride)).to.eql([]);
    expect(([] as API.Index[]).sort(sort.compareApiIndex)).to.eql([]);
    expect(([] as API.Field[]).sort(sort.compareApiField)).to.eql([]);
  });

  it("should correctly sort an array of Spec indexes", () => {
    // Sorts first because of collectionGroup
    const a: Spec.Index = {
      collectionGroup: "collectionA",
      queryScope: API.QueryScope.COLLECTION,
      fields: [],
    };

    // fieldA ASCENDING should sort before fieldA DESCENDING
    const b: Spec.Index = {
      collectionGroup: "collectionB",
      queryScope: API.QueryScope.COLLECTION,
      fields: [
        {
          fieldPath: "fieldA",
          order: API.Order.ASCENDING,
        },
      ],
    };

    // This compound index sorts before the following simple
    // index because the first element sorts first.
    const c: Spec.Index = {
      collectionGroup: "collectionB",
      queryScope: API.QueryScope.COLLECTION,
      fields: [
        {
          fieldPath: "fieldA",
          order: API.Order.ASCENDING,
        },
        {
          fieldPath: "fieldB",
          order: API.Order.ASCENDING,
        },
      ],
    };

    const d: Spec.Index = {
      collectionGroup: "collectionB",
      queryScope: API.QueryScope.COLLECTION,
      fields: [
        {
          fieldPath: "fieldB",
          order: API.Order.ASCENDING,
        },
      ],
    };

    const e: Spec.Index = {
      collectionGroup: "collectionB",
      queryScope: API.QueryScope.COLLECTION,
      fields: [
        {
          fieldPath: "fieldB",
          order: API.Order.ASCENDING,
        },
        {
          fieldPath: "fieldA",
          order: API.Order.ASCENDING,
        },
      ],
    };

    expect([b, a, e, d, c].sort(sort.compareSpecIndex)).to.eql([a, b, c, d, e]);
  });

  it("should correctly sort an array of Spec indexes with apiScope, density, multikey, and unique", () => {
    const a: Spec.Index = {
      collectionGroup: "collectionA",
      queryScope: API.QueryScope.COLLECTION,
      fields: [],
      apiScope: API.ApiScope.ANY_API,
    };

    const b: Spec.Index = {
      collectionGroup: "collectionA",
      queryScope: API.QueryScope.COLLECTION,
      fields: [],
      apiScope: API.ApiScope.ANY_API,
    };

    const c: Spec.Index = {
      collectionGroup: "collectionA",
      queryScope: API.QueryScope.COLLECTION,
      fields: [],
      apiScope: API.ApiScope.DATASTORE_MODE_API,
    };

    const d: Spec.Index = {
      collectionGroup: "collectionA",
      queryScope: API.QueryScope.COLLECTION,
      fields: [],
      apiScope: API.ApiScope.MONGODB_COMPATIBLE_API,
    };

    const e: Spec.Index = {
      collectionGroup: "collectionA",
      queryScope: API.QueryScope.COLLECTION,
      fields: [],
      apiScope: API.ApiScope.MONGODB_COMPATIBLE_API,
      density: API.Density.DENSITY_UNSPECIFIED,
    };

    const f: Spec.Index = {
      collectionGroup: "collectionA",
      queryScope: API.QueryScope.COLLECTION,
      fields: [],
      apiScope: API.ApiScope.MONGODB_COMPATIBLE_API,
      density: API.Density.SPARSE_ALL,
    };

    const g: Spec.Index = {
      collectionGroup: "collectionA",
      queryScope: API.QueryScope.COLLECTION,
      fields: [],
      apiScope: API.ApiScope.MONGODB_COMPATIBLE_API,
      density: API.Density.SPARSE_ANY,
    };

    const h: Spec.Index = {
      collectionGroup: "collectionA",
      queryScope: API.QueryScope.COLLECTION,
      fields: [],
      apiScope: API.ApiScope.MONGODB_COMPATIBLE_API,
      density: API.Density.DENSE,
    };

    const i: Spec.Index = {
      collectionGroup: "collectionA",
      queryScope: API.QueryScope.COLLECTION,
      fields: [],
      apiScope: API.ApiScope.MONGODB_COMPATIBLE_API,
      density: API.Density.DENSE,
      multikey: false,
    };

    const j: Spec.Index = {
      collectionGroup: "collectionA",
      queryScope: API.QueryScope.COLLECTION,
      fields: [],
      apiScope: API.ApiScope.MONGODB_COMPATIBLE_API,
      density: API.Density.DENSE,
      multikey: true,
    };

    const k: Spec.Index = {
      collectionGroup: "collectionA",
      queryScope: API.QueryScope.COLLECTION,
      fields: [],
      apiScope: API.ApiScope.MONGODB_COMPATIBLE_API,
      density: API.Density.DENSE,
      multikey: true,
      unique: false,
    };

    const l: Spec.Index = {
      collectionGroup: "collectionA",
      queryScope: API.QueryScope.COLLECTION,
      fields: [],
      apiScope: API.ApiScope.MONGODB_COMPATIBLE_API,
      density: API.Density.DENSE,
      multikey: true,
      unique: true,
    };

    expect([l, k, j, i, h, g, f, e, d, c, b, a].sort(sort.compareSpecIndex)).to.eql([
      a,
      b,
      c,
      d,
      e,
      f,
      g,
      h,
      i,
      j,
      k,
      l,
    ]);
  });

  it("should correcty sort an array of Spec field overrides", () => {
    // Sorts first because of collectionGroup
    const a: Spec.FieldOverride = {
      collectionGroup: "collectionA",
      fieldPath: "fieldA",
      indexes: [],
    };

    const b: Spec.FieldOverride = {
      collectionGroup: "collectionB",
      fieldPath: "fieldA",
      indexes: [],
    };

    // Order indexes sort before Array indexes
    const c: Spec.FieldOverride = {
      collectionGroup: "collectionB",
      fieldPath: "fieldB",
      indexes: [
        {
          queryScope: API.QueryScope.COLLECTION,
          order: API.Order.ASCENDING,
        },
      ],
    };

    const d: Spec.FieldOverride = {
      collectionGroup: "collectionB",
      fieldPath: "fieldB",
      indexes: [
        {
          queryScope: API.QueryScope.COLLECTION,
          arrayConfig: API.ArrayConfig.CONTAINS,
        },
      ],
    };

    expect([b, a, d, c].sort(sort.compareFieldOverride)).to.eql([a, b, c, d]);
  });

  it("should sort ttl true to be last in an array of Spec field overrides", () => {
    // Sorts first because of collectionGroup
    const a: Spec.FieldOverride = {
      collectionGroup: "collectionA",
      fieldPath: "fieldA",
      ttl: false,
      indexes: [],
    };
    const b: Spec.FieldOverride = {
      collectionGroup: "collectionA",
      fieldPath: "fieldB",
      ttl: true,
      indexes: [],
    };
    const c: Spec.FieldOverride = {
      collectionGroup: "collectionB",
      fieldPath: "fieldA",
      ttl: false,
      indexes: [],
    };
    const d: Spec.FieldOverride = {
      collectionGroup: "collectionB",
      fieldPath: "fieldB",
      ttl: true,
      indexes: [],
    };
    expect([b, a, d, c].sort(sort.compareFieldOverride)).to.eql([a, b, c, d]);
  });

  it("should correctly sort an array of API indexes", () => {
    // Sorts first because of collectionGroup
    const a: API.Index = {
      name: "/projects/project/databases/(default)/collectionGroups/collectionA/indexes/a",
      queryScope: API.QueryScope.COLLECTION,
      fields: [],
    };

    // fieldA ASCENDING should sort before fieldA DESCENDING
    const b: API.Index = {
      name: "/projects/project/databases/(default)/collectionGroups/collectionB/indexes/b",
      queryScope: API.QueryScope.COLLECTION,
      fields: [
        {
          fieldPath: "fieldA",
          order: API.Order.ASCENDING,
        },
      ],
    };

    // This compound index sorts before the following simple
    // index because the first element sorts first.
    const c: API.Index = {
      name: "/projects/project/databases/(default)/collectionGroups/collectionB/indexes/c",
      queryScope: API.QueryScope.COLLECTION,
      fields: [
        {
          fieldPath: "fieldA",
          order: API.Order.ASCENDING,
        },
        {
          fieldPath: "fieldB",
          order: API.Order.ASCENDING,
        },
      ],
    };

    const d: API.Index = {
      name: "/projects/project/databases/(default)/collectionGroups/collectionB/indexes/d",
      queryScope: API.QueryScope.COLLECTION,
      fields: [
        {
          fieldPath: "fieldA",
          order: API.Order.DESCENDING,
        },
      ],
    };

    const e: API.Index = {
      name: "/projects/project/databases/(default)/collectionGroups/collectionB/indexes/e",
      queryScope: API.QueryScope.COLLECTION,
      fields: [
        {
          fieldPath: "fieldA",
          vectorConfig: {
            dimension: 100,
            flat: {},
          },
        },
      ],
    };

    const f: API.Index = {
      name: "/projects/project/databases/(default)/collectionGroups/collectionB/indexes/f",
      queryScope: API.QueryScope.COLLECTION,
      fields: [
        {
          fieldPath: "fieldA",
          vectorConfig: {
            dimension: 200,
            flat: {},
          },
        },
      ],
    };

    // This Index is invalid, but is used to verify sort ordering on undefined
    // fields.
    const g: API.Index = {
      name: "/projects/project/databases/(default)/collectionGroups/collectionB/indexes/g",
      queryScope: API.QueryScope.COLLECTION,
      fields: [
        {
          fieldPath: "fieldA",
        },
      ],
    };

    expect([b, a, d, g, f, e, c].sort(sort.compareApiIndex)).to.eql([a, b, c, d, e, f, g]);
  });

  it("should correctly sort an array of API field overrides", () => {
    // Sorts first because of collectionGroup
    const a: API.Field = {
      name: "/projects/myproject/databases/(default)/collectionGroups/collectionA/fields/fieldA",
      indexConfig: {
        indexes: [],
      },
    };

    const b: API.Field = {
      name: "/projects/myproject/databases/(default)/collectionGroups/collectionB/fields/fieldA",
      indexConfig: {
        indexes: [],
      },
    };

    // Order indexes sort before Array indexes
    const c: API.Field = {
      name: "/projects/myproject/databases/(default)/collectionGroups/collectionB/fields/fieldB",
      indexConfig: {
        indexes: [
          {
            queryScope: API.QueryScope.COLLECTION,
            fields: [{ fieldPath: "fieldB", order: API.Order.DESCENDING }],
          },
        ],
      },
    };

    const d: API.Field = {
      name: "/projects/myproject/databases/(default)/collectionGroups/collectionB/fields/fieldB",
      indexConfig: {
        indexes: [
          {
            queryScope: API.QueryScope.COLLECTION,
            fields: [{ fieldPath: "fieldB", arrayConfig: API.ArrayConfig.CONTAINS }],
          },
        ],
      },
    };

    expect([b, a, d, c].sort(sort.compareApiField)).to.eql([a, b, c, d]);
  });
});
